<!DOCTYPE html>
<html lang="zh-CN">





<head>
  <meta charset="UTF-8">
  <link rel="apple-touch-icon" sizes="76x76" href="/img/apple-touch-icon.png">
  <link rel="icon" type="image/png" href="/img/favicon.png">
  <meta name="viewport"
        content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  
  <meta name="description" content="安全行业从业者，主要方向PC逆向附带安卓和Linux逆向，时不时喜欢写点BUG代码">
  <meta name="author" content="Cray">
  <meta name="keywords" content="">
  <title>编写基础调试器：第二部分（断点/单步断点） ~ 逆向安全博客</title>

  <link rel="stylesheet" href="https://cdn.staticfile.org/font-awesome/5.12.1/css/all.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/css/bootstrap.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/mdbootstrap/4.13.0/css/mdb.min.css"  >
<link rel="stylesheet" href="https://cdn.staticfile.org/github-markdown-css/3.0.1/github-markdown.min.css"  >

<link rel="stylesheet" href="//at.alicdn.com/t/font_1067060_qzomjdt8bmp.css">



  <link rel="stylesheet" href="/lib/prettify/tomorrow.min.css"  >

<link rel="stylesheet" href="/css/main.css"  >


  <link rel="stylesheet" href="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.css"  >


<meta name="generator" content="Hexo 5.2.0"></head>


<body>
  <header style="height: 70vh;">
    <nav id="navbar" class="navbar fixed-top  navbar-expand-lg navbar-dark scrolling-navbar">
  <div class="container">


    <button id="navbar-toggler-btn" class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent"
            aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
      <div class="animated-icon"><span></span><span></span><span></span></div>
    </button>

    <!-- Collapsible content -->
    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      <ul class="navbar-nav ml-auto text-center">
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/">首页</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/archives/">归档</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/tags/">标签</a>
          </li>
        
          
          
          
          
          <li class="nav-item">
            <a class="nav-link" href="/links/">友链</a>
          </li>
        
        
          <li class="nav-item" id="search-btn">
            <a class="nav-link" data-toggle="modal" data-target="#modalSearch">&nbsp;&nbsp;<i
                class="iconfont icon-search"></i>&nbsp;&nbsp;</a>
          </li>
        
      </ul>
    </div>
  </div>
</nav>

    <div class="view intro-2" id="background" false
      style="background: url('https://gitee.com/dancve/BlogImg/raw/master/VM_tools/image-20201123095837741.png') no-repeat center center;
      background-size: cover;">
    
        <div class="full-bg-img">
        <div class="mask rgba-black-light flex-center">
          <div class="container text-center white-text fadeInUp">
            <span class="h2" id="subtitle">
              
                编写基础调试器：第二部分（断点/单步断点）
              
            </span>

            
              <br>
              

              <p>
                
                  
                  &nbsp;<i class="far fa-chart-bar"></i>
                  <span class="post-count">
                    2.9k 字
                  </span>&nbsp;
                

                
                  
                  &nbsp;<i class="far fa-clock"></i>
                  <span class="post-count">
                      11 分钟
                  </span>&nbsp;
                

                
                  <!-- 不蒜子统计文章PV -->
                  
                  &nbsp;<i class="far fa-eye" aria-hidden="true"></i>&nbsp;
                  <span id="busuanzi_container_page_pv">
                    <span id="busuanzi_value_page_pv"></span> 次
                  </span>&nbsp;
                
              </p>
            
          </div>

          
        </div>
      </div>
    </div>
  </header>

  <main>
    
      

<div class="container-fluid">
  <div class="row">
    <div class="d-none d-lg-block col-lg-2"></div>
    <div class="col-lg-8 nopadding-md">
      <div class="py-5 z-depth-3" id="board">
        <div class="post-content mx-auto" id="post">
          <div class="markdown-body">
            <p>为了使调试器具有实际用途，它需要具有暂停，检查和恢复目标进程的能力。这篇文章将讨论在x86和x64 Windows平台上的大多数调试器如何实现断点以及如何执行指令级步进。</p>
<h3 id="断点"><a href="#断点" class="headerlink" title="断点"></a>断点</h3><p>断点可以简单地定义为让正在执行的代码中故意中断执行的位置。在调试器的上下文中，这很有用，因为它允许调试器在什么也没有发生的时间（即过程实际上“中断”）时检查过程。当目标处于中断状态时，调试器能拥有的几个基本功能是查看和修改寄存器/内存，打印断点周围区域的反汇编指令，步入或步过一条指令，和其他相关的诊断功能。断点本身可以有几种不同的形式。</p>
<h4 id="Hardware-Interrupt-Breakpoints-硬件中断断点"><a href="#Hardware-Interrupt-Breakpoints-硬件中断断点" class="headerlink" title="Hardware Interrupt Breakpoints(硬件中断断点)"></a>Hardware Interrupt Breakpoints(硬件中断断点)</h4><p>硬件中断断点可能是在调试器中了解 和实现的最常见和最简单的断点。这些特定于x86和x64体系结构，并通过使用硬件支持的指令来实现，该指令专门用于生成调试器的软件中断。该指令的操作码为<code>0xCC</code>，也就是汇编中的<code>INT 3</code>指令。大多数调试器实现此目的的方式是将所需地址（即断点地址）处的操作码替换为<code>0xCC</code>操作码。调用代码后，会触发<strong>EXCEPTION_BREAKPOINT</strong>中断，调试器将捕获并提供用户执行调试功能的选项。当用户完成自己的操作后且希望继续时，调试器将把原始指令替换回去，确保执行地址（<em>EIP</em>或<em>RIP</em>寄存器，取决于体系结构）指向该原始地址，并且继续执行。</p>
<p>但是，出现了一个有趣的问题。当原始指令被换回时，断点将丢失。如果断点仅被命中一次，这可能没问题，但这种情况很少发生。此后需要一种方法可以立即重新启用该断点。这是通过设置<em>EFlags</em>（或x64的<em>RFlags</em>）寄存器将处理器设置为单步模式来完成的。幸运的是，通过启用第8位（即与<em>0x100</em>进行“<em>或”运算</em>）很容易实现。当*<strong>EXCEPTION_BREAKPOINT**<em>异常被处理并恢复执行，将有另一个异常，</em></strong>EXCEPTION_SINGLE_STEP***，引发下一条执行的指令。此时，调试器可以重新启用上一条指令的断点并恢复执行。</p>
<blockquote>
<p>单步断点是通过计算机架构中的存在的EFlags设定，后面的会讲到</p>
</blockquote>
<p>在Windows平台上，通过检查工作中的<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms679297(v=vs.85).aspx">DebugBreak</a>函数很容易发现这些。此功能旨在触发本地（在同一进程中）断点。下面是该函数的反汇编，表明它只是上面描述的内容。那些对<em>mov edi，edi</em>指令感到疑惑的人可以阅读有关热补丁的更多信息，可以参考此处<a target="_blank" rel="noopener" href="http://blogs.msdn.com/b/ishai/archive/2004/06/24/165143.aspx">hot-patch point</a>.</p>
<pre><code class="asm">_DebugBreak @ 0：
752C3C5D 8B FF mov edi,edi  
752C3C5F CC int 3  
752C3C60 C3 ret  </code></pre>
<h4 id="Hardware-Debug-Registers-硬件调试寄存器"><a href="#Hardware-Debug-Registers-硬件调试寄存器" class="headerlink" title="Hardware Debug Registers(硬件调试寄存器)"></a>Hardware Debug Registers(硬件调试寄存器)</h4><p>第二种断点实现技术也特定于x86和x64体系结构，并利用了指令集提供的特定调试寄存器。这些是调试寄存器*<strong>DR0**<em>，</em></strong>DR1*<strong>，**<em>DR2</em></strong>，*<strong>DR3**<em>和</em></strong>DR7*<strong>。前四个寄存器用于保存需要断下的地址。这意味着整个程序最多只能有四个有效的硬件断点。**<em>DR7</em></strong>寄存器用于控制这四个寄存器的使能，位<em>0、2、4、6</em>对应于<em>DR0</em>，…，<em>DR3的</em>开/关。此外，位16-17、20-21、24-25和28-29充当<em>DR0</em>，…，<em>DR3</em>的位掩码，用于触发这些断点，执行时为00，读取时为01，写入时为11 。</p>
<p>在Windows平台上设置这些断点有些棘手。它们<strong>必须</strong>在进程主线程上设置。这涉及获取主线程，使用至少*<strong>THREAD_GET_CONTEXT**<em>和</em></strong>THREAD_SET_CONTEXT*<strong>特权打开该线程的句柄，并使用<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms679362(v=vs.85).aspx">GetThreadContext</a> / <a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms680632(v=vs.85).aspx">SetThreadContext</a>获取/设置 带有修改后的线程线程上下文。可以发现，这种断点不用修改内存中的可执行代码。这与之前必须替换操作码的情况不同。这些是通过更改硬件寄存器的内容来设置和取消设置的断点。命中这些断点后会引发**<em>EXCEPTION_SINGLE_STEP</em></strong>异常 ，然后调试器将以与上一节中的方式几乎相同的方式对其进行处理。由于数量的限制，这些内容不会在本文的示例断点代码中显示，但是出于完整性的考虑，最终可能会在以后写出来。我已经写了自己的使用情况在API钩子<a target="_blank" rel="noopener" href="http://www.codereversing.com/blog/?p=76">Here</a>。调试器的实现与那里的实现非常接近。</p>
<h4 id="软件断点"><a href="#软件断点" class="headerlink" title="软件断点"></a>软件断点</h4><p>最后一类断点完全在软件中执行，并且与操作系统的功能密切相关。它们的另一个名称是内存断点。它们结合了中断断点的一些最佳功能，即具有任意数量的能力，以及硬件断点的最佳功能，这就是执行代码中的任何内容都不需要覆盖。但是，存在一个主要缺点：由于其实现，它们使代码的执行速度大大降低。</p>
<p>该断点不是在<strong>地址</strong>级别实现的，而是在<strong>页</strong>级别实现的。这些工作原理是使用<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa366549(v=vs.85).aspx">guard page</a>将要设置断点的地址的页面权限更改为<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/aa366899(v=vs.85).aspx">VirtualProtectEx</a>页面权限。当页面上的<strong>任何</strong>指令将被访问时，将抛出**<em>EXCEPTION_GUARD_PAGE**</em>异常。调试器将处理此异常，并检查异常地址是否为断点地址。如果是这样，调试器可以像处理任何其他断点一样执行用户操作。如果不是，则调试器必须执行一些额外的步骤。</p>
<p>根据文档，保护页保护将在提升后从页面上删除。这意味着一旦处理了异常并继续执行，此后的任何访问都不会再抛出*<strong>EXCEPTION_GUARD_PAGE**<em>异常。因此，如果访问的指令不是所需的断点地址，则断点将丢失。为了解决这个问题，将使用类似于硬件中断断点部分中介绍的技术。处理器将进入单步模式并继续执行。在下一条指令上，将有一个</em></strong>EXCEPTION_SINGLE_STEP***引发异常。这将由调试器处理，并且将在页面上重新启用保护页面属性。此实现也将不在本文中讨论，但将来可能会涉及。我以前写过关于它，也是在API挂钩的情况下，<a target="_blank" rel="noopener" href="http://www.codereversing.com/blog/?p=79">Here</a>。</p>
<h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><p>如上所述，启用和禁用硬件中断断点仅是用<em>0xCC</em>（<em>INT 3</em>）覆盖操作码的问题。在Windows中，就是非常简单的通过<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms680553(v=vs.85).aspx">ReadProcessMemory</a>和<a target="_blank" rel="noopener" href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms681674(v=vs.85).aspx">WriteProcessMemory</a>来完成。</p>
<pre><code class="c++">const bool InterruptBreakpoint::EnableBreakpoint()
&#123;
    SIZE_T ulBytes = 0;
    bool bSuccess = BOOLIFY(ReadProcessMemory(m_hProcess, (LPCVOID)m_dwAddress, &amp;m_originalByte, sizeof(unsigned char), &amp;ulBytes));
    if (bSuccess &amp;&amp; ulBytes == sizeof(unsigned char))
    &#123;
        bSuccess = BOOLIFY(WriteProcessMemory(m_hProcess, (LPVOID)m_dwAddress, &amp;m_breakpointOpcode, sizeof(unsigned char), &amp;ulBytes));
        return bSuccess &amp;&amp; (ulBytes == sizeof(unsigned char));
    &#125;
    else
    &#123;
        fprintf(stderr, &quot;Could not read from address %p. Error = %X\n&quot;, m_dwAddress, GetLastError());
    &#125;

    return false;
&#125;</code></pre>
<p>读取和存储目标地址中的原始字节，然后用<strong>0xCC</strong>覆盖。 没有什么与众不同的操作。在禁用断点时，只需执行相反的操作即写回原始字节就行。</p>
<pre><code class="C++">const bool InterruptBreakpoint::DisableBreakpoint()
&#123;
    SIZE_T ulBytes = 0;
    const bool bSuccess = BOOLIFY(WriteProcessMemory(m_hProcess, (LPVOID)m_dwAddress, &amp;m_originalByte, sizeof(unsigned char), &amp;ulBytes));
    if (bSuccess &amp;&amp; ulBytes == sizeof(unsigned char))
    &#123;
        return true;
    &#125;
    fprintf(stderr, &quot;Could not write back original opcode to address %p. Error = %X\n&quot;, m_dwAddress, GetLastError());

    return false;
&#125;</code></pre>
<p>现在具有启用/禁用断点的功能，现在该看一下处理程序了。如上所述，访问断点所在的指令后，将引发**<em>EXCEPTION_BREAKPOINT**</em>异常。处理这个异常需要下面几个步骤：</p>
<ol>
<li>检查断点是否在断点列表中。这样做是因为在首次附加调试器时，将从调试器附加线程中引发**<em>EXCEPTION_BREAKPOINT**</em>（请参阅上一篇文章）。我们不在乎此异常，因此只需跳过它并继续执行</li>
<li>如果它在断点列表中，说明是用户设置的断点，则需要响应它。如前所述，这是为了允许执行原始指令。</li>
<li>打开线程的句柄并获取线程上下文。更改执行指针（<em>EIP</em>或<em>RIP</em>）以指向断点地址，并启用单步模式。将线程上下文设置为此新修改的上下文</li>
<li>保存此断点。这是为了下一次再次到这个地址也能产生**<em>EXCEPTION_SINGLE_STEP**</em>异常。</li>
<li>提示用户继续或执行单步操作。等待他们的响应，然后根据选择继续执行。</li>
</ol>
<p>放入代码，如下所示：</p>
<pre><code class="C++">Register(DebugExceptions::eBreakpoint, [&amp;](const DEBUG_EVENT &amp;dbgEvent)
&#123;
    auto &amp;exceptionRecord = dbgEvent.u.Exception.ExceptionRecord;
    const DWORD_PTR dwExceptionAddress = (DWORD_PTR)exceptionRecord.ExceptionAddress;
    fprintf(stderr, &quot;Received breakpoint at address %p.\n&quot;, dwExceptionAddress);  

    Breakpoint *pBreakpoint = m_pDebugger-&gt;FindBreakpoint(dwExceptionAddress);
    if (pBreakpoint != nullptr)
    &#123;
        if (pBreakpoint-&gt;Disable())
        &#123;
            CONTEXT ctx = &#123; 0 &#125;;
            ctx.ContextFlags = CONTEXT_ALL;
            HANDLE hThread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, FALSE, dbgEvent.dwThreadId);
            if (hThread != NULL)
            &#123;
                (void)GetThreadContext(hThread, &amp;ctx);
#ifdef _M_IX86
                ctx.Eip = (DWORD_PTR)dwExceptionAddress;
#elif defined _M_AMD64
                ctx.Rip = (DWORD_PTR)dwExceptionAddress;
#else
#error &quot;Unsupported architecture&quot;
#endif
                ctx.EFlags |= 0x100;
                m_pDebugger-&gt;m_pLastBreakpoint = pBreakpoint;
                m_pDebugger-&gt;m_dwExecutingThreadId = dbgEvent.dwThreadId;
                (void)SetThreadContext(hThread, &amp;ctx);

                fprintf(stderr, &quot;Press c to continue or s to begin stepping.\n&quot;);
                (void)m_pDebugger-&gt;WaitForContinue();
            &#125;
            else
            &#123;
                fprintf(stderr, &quot;Could not open handle to thread %p. Error = %X\n&quot;, dbgEvent.dwThreadId, GetLastError());
            &#125;
        &#125;
        else
        &#123;
            fprintf(stderr, &quot;Could not remove breakpoint at address %p.&quot;, dwExceptionAddress);
        &#125;
    &#125;
    SetContinueStatus(DBG_CONTINUE);
&#125;);</code></pre>
<p><strong><em>EXCEPTION_SINGLE_STEP</em></strong>异常的处理程序有两个用途：在该程序中可以重新启用前一个命中的断点，并且还可以使用户继续单步执行程序。最后需要声明一个Eflags的一个标志位，该标志位在用户希望进入单步模式时设置（这里的单步模式是通过每次都设置eflags的标置位完成）。如果用户处于单步模式，则向他们提示并等待输入以继续单步执行或继续执行。同样，将其放入代码中，如下所示：</p>
<pre><code class="C++">Register(DebugExceptions::eSingleStep, [&amp;](const DEBUG_EVENT &amp;dbgEvent)
&#123;
    auto &amp;exceptionRecord = dbgEvent.u.Exception.ExceptionRecord;
    const DWORD_PTR dwExceptionAddress = (DWORD_PTR)exceptionRecord.ExceptionAddress;
    fprintf(stderr, &quot;Received single step at address %p\n&quot;, dwExceptionAddress);
    if (m_pDebugger-&gt;m_bIsStepping)
    &#123;
        fprintf(stderr, &quot;Press s to continue stepping.\n&quot;);
        m_pDebugger-&gt;m_dwExecutingThreadId = dbgEvent.dwThreadId;
        (void)m_pDebugger-&gt;WaitForContinue();
    &#125;
    if (!m_pDebugger-&gt;m_pLastBreakpoint-&gt;IsEnabled())
    &#123;
        (void)m_pDebugger-&gt;m_pLastBreakpoint-&gt;Enable();
    &#125;

    SetContinueStatus(DBG_CONTINUE);
&#125;);</code></pre>
<h4 id="调试器实例"><a href="#调试器实例" class="headerlink" title="调试器实例"></a>调试器实例</h4><p>一切就绪后，调试器就可以进行测试了。最简单的方法是编写一个示例程序，让调和器附加到该程序上：</p>
<pre><code class="C++">#include &lt;stdio.h&gt;
#include &lt;Windows.h&gt;

void TestFunction()
&#123;
    printf(&quot;Hello, World!\n&quot;);
&#125;

int main(int argc, char *argv[])
&#123;
    printf(&quot;Test function address: %p\n&quot;, TestFunction);
    while (true)
    &#123;
        getchar();
        TestFunction();
    &#125;

    return 0;
&#125;</code></pre>
<p>在我主机上，<strong>TestFunction</strong>位于<strong>0x13B1000</strong>。在此地址上附加并设置断点后，调试器便能够成单步执行程序或直接继续执行下去。</p>
<pre><code class="json">a
Target address: 0x13B1000
Received breakpoint at address 13B1000.
Press c to continue or s to begin stepping.
s
Received single step at address 13B1001
Press s to continue stepping.
s
Received single step at address 13B1003
Press s to continue stepping.
s
Received single step at address 13B1009
Press s to continue stepping.
s
Received single step at address 13B100A
Press s to continue stepping.
c</code></pre>
<p>上面单步执行的地址也和反汇编匹配。</p>
<pre><code class="asm">013B1000 55 push ebp   
013B1001 8B EC mov ebp，esp   
013B1003 81 EC C0 00 00 00 sub esp，0C0h   
013B1009 53 push ebx   
013B100A 56 push esi  </code></pre>
<p>在x64版本（带有调试器的x64版本）上进行了测试，并获得了相同的结果。</p>
<p>原文链接：<a target="_blank" rel="noopener" href="http://www.codereversing.com/blog/archives/169">http://www.codereversing.com/blog/archives/169</a></p>

            <hr>
          </div>
          <br>
          <div>
            <p>
            
            
              <span>
                <i class="iconfont icon-tag"></i>
                
                  <a class="hover-with-bg" href="/tags/%E9%80%86%E5%90%91/">逆向</a>
                
              </span>
            
            </p>
            
              <p class="note note-warning">本博客所有文章除特别声明外，均采用 <a target="_blank" href="https://zh.wikipedia.org/wiki/Wikipedia:CC_BY-SA_3.0%E5%8D%8F%E8%AE%AE%E6%96%87%E6%9C%AC" rel="nofollow noopener noopener">CC BY-SA 3.0协议</a> 。转载请注明出处！</p>
            
          </div>
        </div>
      </div>
    </div>
    
      <div class="d-none d-lg-block col-lg-2 toc-container">
        <div id="toc">
  <p class="h5"><i class="far fa-list-alt"></i>&nbsp;目录</p>
  <div id="tocbot"></div>
</div>
      </div>
    
  </div>
</div>

<!-- custom -->


<!-- Comments -->
<div class="col-lg-7 mx-auto nopadding-md">
  <div class="container comments mx-auto" id="comments">
    
      <br><br>
      
      
  <div class="disqus" style="width:100%">
    <div id="disqus_thread"></div>
    <script>
      var disqus_config = function () {
        this.page.url = 'http://cve.gitee.io/cve/2020/11/21/编写基础调试器：第二部分（断点_单步）/';
        this.page.identifier = '/2020/11/21/编写基础调试器：第二部分（断点_单步）/';
      };
      var oldLoad = window.onload;
      window.onload = function () {
        var d = document, s = d.createElement('script');
        s.type = 'text/javascript';
        s.src = '//' + 'crayon-1' + '.disqus.com/embed.js';
        s.setAttribute('data-timestamp', +new Date());
        (d.head || d.body).appendChild(s);
      };
    </script>
    <noscript>Please enable JavaScript to view the <a target="_blank" href="https://disqus.com/?ref_noscript" rel="nofollow noopener noopener">comments
        powered by Disqus.</a></noscript>
  </div>


    
  </div>
</div>

    
  </main>

  
    <a class="z-depth-1" id="scroll-top-button" href="#" role="button">
      <i class="fa fa-chevron-up scroll-top-arrow" aria-hidden="true"></i>
    </a>
  

  
    <div class="modal fade" id="modalSearch" tabindex="-1" role="dialog" aria-labelledby="ModalLabel"
     aria-hidden="true">
  <div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
    <div class="modal-content">
      <div class="modal-header text-center">
        <h4 class="modal-title w-100 font-weight-bold">搜索</h4>
        <button type="button" id="local-search-close" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
      <div class="modal-body mx-3">
        <div class="md-form mb-5">
          <input type="text" id="local-search-input" class="form-control validate">
          <label data-error="x" data-success="v"
                 for="local-search-input">关键词</label>
        </div>
        <div class="list-group" id="local-search-result"></div>
      </div>
    </div>
  </div>
</div>
  

  <footer class="mt-5">
  <div class="text-center py-3">
    <a href="https://hexo.io" target="_blank" rel="nofollow noopener"><b>Hexo</b></a>
    <i class="iconfont icon-love"></i>
    <a href="https://github.com/fluid-dev/hexo-theme-fluid" target="_blank" rel="nofollow noopener"> <b>Fluid</b></a>
    <br>

    
  
    <!-- 不蒜子统计PV -->
    
    &nbsp;<span id="busuanzi_container_site_pv"></span>总访问量 
          <span id="busuanzi_value_site_pv"></span> 次&nbsp;
  
  
    <!-- 不蒜子统计UV -->
    
    &nbsp;<span id="busuanzi_container_site_uv"></span>总访客数 
            <span id="busuanzi_value_site_uv"></span> 人&nbsp;
  
  <br>



    


    <!-- cnzz Analytics icon -->
    

  </div>
</footer>

<!-- SCRIPTS -->
<script src="https://cdn.staticfile.org/jquery/3.4.1/jquery.min.js" ></script>
<script src="https://cdn.staticfile.org/popper.js/1.16.1/umd/popper.min.js" ></script>
<script src="https://cdn.staticfile.org/twitter-bootstrap/4.4.1/js/bootstrap.min.js" ></script>
<script src="https://cdn.staticfile.org/mdbootstrap/4.13.0/js/mdb.min.js" ></script>
<script src="/js/main.js" ></script>


  <script src="/js/lazyload.js" ></script>



  
  <script src="https://cdn.staticfile.org/tocbot/4.10.0/tocbot.min.js" ></script>
  <script>
    $(document).ready(function () {
      var navHeight = $('#navbar').height();
      var toc = $('#toc');
      var main = $('main');
      var tocT = navHeight + (toc.offset().top - main.offset().top);
      var tocLimMin = main.offset().top - navHeight;
      var tocLimMax = $('#comments').offset().top - navHeight;
      $(window).scroll(function () {
        var scroH = document.body.scrollTop + document.documentElement.scrollTop;
        if (tocLimMin <= scroH && scroH <= tocLimMax) {
          toc.css({
            'display': 'block',
            'position': 'fixed',
            'top': tocT,
          });
        } else if (scroH <= tocLimMin) {
          toc.css({
            'position': '',
            'top': '',
          });
        } else if (scroH > tocLimMax) {
          toc.css('display', 'none');
        }
      });
      tocbot.init({
        tocSelector: '#tocbot',
        contentSelector: '.post-content',
        headingSelector: 'h1,h2,h3,h4,h5,h6',
        linkClass: 'tocbot-link',
        activeLinkClass: 'tocbot-active-link',
        listClass: 'tocbot-list',
        isCollapsedClass: 'tocbot-is-collapsed',
        collapsibleClass: 'tocbot-is-collapsible',
        scrollSmooth: true,
      });
      if ($('.toc-list-item').length > 0) {
        $('#toc > p').css('visibility', 'visible');
      }
    });
  </script>







  <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js" ></script>


<!-- Plugins -->



  <script src="https://cdn.staticfile.org/prettify/188.0.0/prettify.min.js" ></script>
  <script>
    $(document).ready(function () {
      $('pre').addClass('prettyprint  linenums');
      prettyPrint();
    })
  </script>





  <script src="https://cdn.staticfile.org/anchor-js/4.2.2/anchor.min.js" ></script>
  <script>
    anchors.options = {
      placement: "right",
      visible: "hover",
      
    };
    var el = "h1,h2,h3,h4,h5,h6".split(",");
    var res = [];
    for (item of el) {
      res.push(".markdown-body > " + item)
    }
    anchors.add(res.join(", "))
  </script>



  <script src="/js/local-search.js" ></script>
  <script>
    var path = "/local-search.xml";
    var inputArea = document.querySelector("#local-search-input");
    inputArea.onclick = function () {
      getSearchFile(path);
      this.onclick = null
    }
  </script>



  <script src="https://cdn.staticfile.org/fancybox/3.5.7/jquery.fancybox.min.js" ></script>
  <script>
    $("#post img:not(.no-zoom img, img[no-zoom])").each(
      function () {
        var element = document.createElement("a");
        $(element).attr("data-fancybox", "images");
        $(element).attr("href", $(this).attr("src"));
        $(this).wrap(element);
      }
    );
  </script>












</body>
</html>
